classdef dotsPhenomenon
    % @class dotsPhenomenon
    % Represents a region in finite, discrete, n-dimensional space.
    % @details
    % dotsPhenomenon objects represent regions in space, similar to events
    % in probability theory.  Since "event" has other meanings in science
    % and computer science, Snow Dots calls these regions "phenomenons".
    % @details
    % dotsPhenomenon spaces must be finite and discrete.  Each point in
    % space may either belong or not belong to a region of interest.  Thus,
    % it is equivalent to think of a dotsPhenomenon object as managing an
    % n-dimensional array of true/false values.
    % @details
    % For example, a simple dotsPhenomenon might represent a 2-dimensional
    % rectangular region.  It might partition the region along one
    % dimension at a loction given by a partition value.  The result would
    % be two rectangular sub-regions.  The object's true/false array would
    % be divided into a "true" side and a "false" side.
    % @details
    % Simple phenomenon objects can combine to make complex ones.  The
    % object above might combine with another object that partitions the
    % same space in a different way.  An "intersection" combination will
    % merge the true and false regions with AND logic.  A "union"
    % combination will merge the true and false regions with OR logic.
    % @details
    % The primary users of dotsPhenomenon objects are probably
    % dotsQueryable objects, which use phenomenons to classify input data.
    % 
    % @ingroup foundation
    
    properties
        % string name to identify this phenomenon
        name = '';
        
        % two-dimensional logical array representing a finite, discrete
        % phenomenon space
        space = false(0,0);
        
        % array of dotsFiniteDimension objects describing each of the
        % dimensions in space
        dimensions = [];
        
        % index identifying the dimension of the phenomenon space that is
        % partitioned.
        partitionDimension = '';
        
        % value along the partitioned dimension of the phenomenon space
        % where a partition is located.
        partitionValue = '';
        
        % string '<', '>', '<=', '>=', or '==' indicating how the
        % phenomenon space is partitioned
        partitionOperator = '';
        
        % cell array of names of phenomenon objects of which this object
        % represents a composite.
        constituentNames = {};
    end
    
    methods
        % Constructor takes no arguments.
        function p = dotsPhenomenon
        end
    end
    
    methods (Static)
        % New simple phenomenon object that represents a space but has no
        % partition, useful as a template for multiple objects that
        % represent the same space.
        % @param name string name for the new object
        % @param dims array of dotsFiniteDimension objects describing each
        % dimensions of the phenomenon's space.
        % @details
        % Creates a new dotsPhenomenon object that represents a discrete,
        % finite space as described by the with the given @a dims.
        % @details
        % The new object will not partition its space in any way.  Use
        % partition() to create new objects that partition the same space.
        function p = template(name, dims)
            p = dotsPhenomenon;
            p.name = name;
            p.dimensions = dims;
            p.space = false([p.dimensions.size]);
        end
        
        % New simple phenomenon object that partitions the same space as
        % the given template object.
        % @param template a dotsPhenomenon object which represents the
        % space to be partitioned.
        % @param dimName string name of the dimension to partition, must
        % match the name of one of the dimensions of @a template.
        % @param partitionValue where to make a partition, along the @a
        % dimName dimension.
        % @ param partitionOperator string indicating how to partition the
        % phenomenon space:
        %   - '<' partition in @a dimName below @a partitionValue
        %   - '>' partition in @a dimName above @a partitionValue
        %   - '<=' partition in @a dimName at @a partitionValue and below
        %   - '>=' partition in @a dimName at @a partitionValue and above
        %   - '==' slice in @a dimName at @a partitionValue exactly
        %   .
        % @details
        % The new object will have the same dimensions as @a template, thus
        % it will represent the same space.  It will make a fresh partition
        % in that space, setting some points to true and others to false.
        % @details
        % The new object will be named automatically to reflect the
        % specified partitioning.  You can rename it if you want.
        function p = partition(template, dimName, partitionValue, partitionOperator)
            dimInd = strcmp(dimName, {template.dimensions.name});
            if ~any(dimInd)
                error(sprintf('phenomenon named %s does not have dimension named %s', ...
                    template.name, dimName))
            end
            
            p = template;
            p.partitionDimension = find(dimInd, 1);
            p.partitionValue = partitionValue;
            p.partitionOperator = partitionOperator;
            
            dimValues = p.dimensions(p.partitionDimension).values;
            dimIndexes = [];
            switch p.partitionOperator
                case '<'
                    dimIndexes = find(dimValues < p.partitionValue);
                case '>'
                    dimIndexes = find(dimValues > p.partitionValue);
                case '<='
                    dimIndexes = find(dimValues <= p.partitionValue);
                case '>='
                    dimIndexes = find(dimValues >= p.partitionValue);
                case '=='
                    dimIndexes = find(dimValues == p.partitionValue);
            end
            
            p.dimensions(p.partitionDimension).indexes = dimIndexes;
            if isempty(dimIndexes)
                % found nothing to partition, set all to false
                p.space(1:end) = false;
                
            else
                % set partitioned region to true
                subInds = {p.dimensions.indexes};
                asignStruct = substruct('()', subInds);
                p.space = subsasgn(p.space, asignStruct, true);
            end
            
            p.name = sprintf('%s%s%.2f', ...
                dimName, p.partitionOperator, p.partitionValue);
        end
        
        % New complex phenomenon object representing a rectangular region.
        % @param template a dotsPhenomenon object which represents the
        % space to be partitioned.
        % @param xDimName string name of the dimension to that contains the
        % width of the rectangular region.  Must match the name of one of
        % the dimensions of @a template.
        % @param yDimName string name of the dimension to that contains the
        % height of the rectangular region.  Must match the name of one of
        % the dimensions of @a template.
        % @param rect matrix of the form [x y width height], where to place
        % the rectangle in the plane specified by @a xDimName and @a
        % yDimName.
        % @param inOut string indicating whether to use the inside
        % (default) or outside of @a rect:
        %	- 'in' set points that fall within @a rect or on the border of
        %	@a rect to true, set other points to false.
        %	- 'out' set points that fall outside of @a rect to true, set
        %	other points to false.
        %   .
        % @details
        % The new object will have the same dimensions as @a template, thus
        % it will represent the same space.  It will make fresh partitions
        % in that space according to @a rect.
        % @details
        % The new object will be named automatically to reflect the given
        % @a rect.  You can rename it if you want.
        function p = rectangle(template, xDimName, yDimName, rect, inOut)
            xGE = dotsPhenomenon.partition(template, ...
                xDimName, rect(1), '>=');
            xLE = dotsPhenomenon.partition(template, ...
                xDimName, rect(1)+rect(3), '<=');
            
            yGE = dotsPhenomenon.partition(template, ...
                yDimName, rect(2), '>=');
            yLE = dotsPhenomenon.partition(template, ...
                yDimName, rect(2)+rect(4), '<=');
            
            inRect = dotsPhenomenon.composite([xGE, xLE, yGE, yLE], 'intersection');
            rectName = sprintf('%.2f ', rect);
            if nargin == 5 && strcmp(inOut, 'out')
                p = dotsPhenomenon.complement(inRect);
                p.name = sprintf('out[%s]', rectName(1:end-1));

            else
                p = inRect;
                p.name = sprintf('in[%s]', rectName(1:end-1));
            end
        end
        
        % New complex phenomenon object that combines the given objects by
        % intersection or union.
        % @param phenoms array of dotsPhenomenon objects from which to make
        % a composite
        % @param how string indicating how to make the compositite:
        %   - 'intersection' combine "true" regions by logical AND, so that
        %   only regions that are true in all of the @phenoms are true in
        %   the new composite.
        %   - 'union' combine "true" regions by logical inclusive OR, so
        %   that any region that is true in any of the @phenoms is true in
        %   the new composite.
        % @details
        % The new object will have the same dimNames, dimSizes, and
        % dimValues as the first of the @phenoms, thus it will represent
        % the same space.
        % @details
        % The new object will be named automatically to reflect the
        % names and combination logic of @a phenoms.  You can rename it if
        % you want.
        function p = composite(phenoms, how)
            p = dotsPhenomenon;
            p.dimensions = phenoms(1).dimensions;
            p.constituentNames = {phenoms.name};
            
            grandN = 1 + length(p.dimensions);
            grandSpace = cat(grandN, phenoms.space);
            switch how
                case 'intersection'
                    p.space = all(grandSpace, grandN);
                    nameFormat = '%s&';
                    
                case 'union'
                    p.space = any(grandSpace, grandN);
                    nameFormat = '%s|';
            end
            nameCat = sprintf(nameFormat, p.constituentNames{:});
            p.name = sprintf('(%s)', nameCat(1:end-1));
        end
        
        % New complex phenomenon object that is the complement of the given
        % object.
        % @param phenom scalar dotsPhenomenon object of which to make the
        % complement.
        % @details
        % The new object will have the same dimNames, dimSizes, and
        % dimValues as the given @phenom, thus it will represent
        % the same space.  Its space will be the logical complement, with
        % all "true" and "false" regions swapped.
        % @details
        % The new object will be named automatically to indicate the
        % negation of @a phenom.  You can rename it if you want.
        function p = complement(phenom)
            p = dotsPhenomenon;
            p.dimensions = phenom.dimensions;
            p.constituentNames = {phenom.name};
            p.space = ~phenom.space;
            p.name = sprintf('(!%s)', phenom.name);
        end
        
        % Utility to get total partition size and total space size of
        % a phenomenon object.
        % @param phenom scalar dotsPhenomenon object
        % @details
        % Returns the total size of all partitioned ("true") regions in the
        % space for the given @a phenom.  Also returns as an optional
        % second argument the size of the phenomenon space.
        % @details
        % This may be handy since dealing with high-dimensional arrays can
        % be cumbersome.
        function [partSize, spaceSize] = getSize(phenom)
            partSize = sum(phenom.space(1:end));
            spaceSize = prod([phenom.dimensions.size]);
        end
    end
end